{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## <center> Open Machine Learning Course mlcourse.ai. English session #1\n",
    "\n",
    "### <center> Autor: Valentin Kovalev"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## <center> Tutorial </center>\n",
    "### <center> Latent Dirichlet Allocation </center>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In this tutorial I'll try some Latent Dirichlet Allocation to automaticallly extract the topics that charactereze texts. <br> Good tuning of LDA (that's an art) can give a really good result on the Leaderboard on kaggle contests with text features. <br>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### import libraries"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from gensim.corpora.dictionary import Dictionary\n",
    "from gensim.models.ldamodel import LdaModel\n",
    "from gensim.test.utils import common_texts"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Extracting topics with LDA\n",
    "\n",
    "LDA represents documents as mix of topics that spit out words with certain probabilities. <br>\n",
    "\n",
    "For each possible topic Z, we'll multiply the frequency of this word type W in Z by the number of other words in document D that already belong to Z. The result will represent the probability that this word came from Z."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Train an LDA model using a Gensim corpus"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Create a corpus from a list of texts"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "common_dictionary = Dictionary(common_texts)\n",
    "common_corpus = [common_dictionary.doc2bow(text) for text in common_texts]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Train the model on the corpus."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "lda = LdaModel(common_corpus, num_topics=10)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### We can save a model to disk, or reload a pre-trained model\n",
    "This code will be commented for not to produce entities"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from gensim.test.utils import datapath"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Save model to disk"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# temp_file = datapath(\"model\")\n",
    "# lda.save(temp_file)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Load a potentially pretrained model from disk."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# lda = LdaModel.load(temp_file)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Check model on using new, unseen documents"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Create a new corpus, made of previously unseen documents."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "other_texts = [\n",
    "    [\"computer\", \"time\", \"graph\"],\n",
    "    [\"survey\", \"response\", \"eps\"],\n",
    "    [\"human\", \"system\", \"computer\"],\n",
    "]\n",
    "other_corpus = [common_dictionary.doc2bow(text) for text in other_texts]\n",
    "\n",
    "unseen_doc = other_corpus[0]\n",
    "vector = lda[unseen_doc]  # get topic probability distribution for a document"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Update the model by incrementally training on the new corpus"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "lda.update(other_corpus)\n",
    "vector = lda[unseen_doc]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### About hyperparameters"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Alpha and Beta Hyperparameters – alpha represents document-topic density and Beta represents topic-word density. Higher the value of alpha, documents are composed of more topics and lower the value of alpha, documents contain fewer topics. On the other hand, higher the beta, topics are composed of a large number of words in the corpus, and with the lower value of beta, they are composed of few words.\n",
    "\n",
    "Number of Topics – Number of topics to be extracted from the corpus. Researchers have developed approaches to obtain an optimal number of topics by using Kullback Leibler Divergence Score. I will not discuss this in detail, as it is too mathematical. For understanding, one can refer to this[1] original paper on the use of KL divergence.\n",
    "\n",
    "Number of Topic Terms – Number of terms composed in a single topic. It is generally decided according to the requirement. If the problem statement talks about extracting themes or concepts, it is recommended to choose a higher number, if problem statement talks about extracting features or terms, a low number is recommended.\n",
    "\n",
    "Number of Iterations / passes – Maximum number of iterations allowed to LDA algorithm for convergence."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Ok, lets check on real data"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "doc1 = \"Sugar is bad to consume. My sister likes to have sugar, but not my father.\"\n",
    "doc2 = \"My father spends a lot of time driving my sister around to dance practice.\"\n",
    "doc3 = \"Doctors suggest that driving may cause increased stress and blood pressure.\"\n",
    "doc4 = \"Sometimes I feel pressure to perform well at school, but my father never seems to drive my sister to do better.\"\n",
    "doc5 = \"Health experts say that Sugar is not good for your lifestyle.\"\n",
    "\n",
    "# compile documents\n",
    "doc_complete = [doc1, doc2, doc3, doc4, doc5]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Cleaning and preprocessing"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import string\n",
    "\n",
    "from nltk.corpus import stopwords\n",
    "from nltk.stem.wordnet import WordNetLemmatizer\n",
    "\n",
    "stop = set(stopwords.words(\"english\"))\n",
    "exclude = set(string.punctuation)\n",
    "lemma = WordNetLemmatizer()\n",
    "\n",
    "\n",
    "def clean(doc):\n",
    "    stop_free = \" \".join([i for i in doc.lower().split() if i not in stop])\n",
    "    punc_free = \"\".join(ch for ch in stop_free if ch not in exclude)\n",
    "    normalized = \" \".join(lemma.lemmatize(word) for word in punc_free.split())\n",
    "    return normalized\n",
    "\n",
    "\n",
    "doc_clean = [clean(doc).split() for doc in doc_complete]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Preparing Document-Term Matrix"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Importing Gensim\n",
    "import gensim\n",
    "from gensim import corpora\n",
    "\n",
    "# Creating the term dictionary of our courpus, where every unique term is assigned an index.\n",
    "dictionary = corpora.Dictionary(doc_clean)\n",
    "\n",
    "# Converting list of documents (corpus) into Document Term Matrix using dictionary prepared above.\n",
    "doc_term_matrix = [dictionary.doc2bow(doc) for doc in doc_clean]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Running LDA Model"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Creating the object for LDA model using gensim library\n",
    "Lda = gensim.models.ldamodel.LdaModel\n",
    "\n",
    "# Running and Trainign LDA model on the document term matrix.\n",
    "ldamodel = Lda(doc_term_matrix, num_topics=3, id2word=dictionary, passes=50)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Results"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(ldamodel.print_topics(num_topics=3, num_words=3))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Topic Modelling for Feature Selection"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Sometimes LDA can also be used as feature selection technique. Take an example of text classification problem where the training data contain category wise documents. If LDA is running on sets of category wise documents. Followed by removing common topic terms across the results of different categories will give the best features for a category."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Bonus: pyLDAvis"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "pyLDAvis is designed to help users interpret the topics in a topic model that has been fit to a corpus of text data. The package extracts information from a fitted LDA topic model to inform an interactive web-based visualization.\n",
    "\n",
    "The visualization is intended to be used within an IPython notebook but can also be saved to a stand-alone HTML file for easy sharing."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "By default the topics are projected to the 2D plane using PCoA on a distance matrix created using the Jensen-Shannon divergence on the topic-term distributions. You can pass in a different multidimensional scaling function via the mds parameter. In addition to pcoa, other provided options are tsne and mmds which operate on the same JS-divergence distance matrix. Both tsne and mmds require that you have sklearn installed. Here is tnse in action:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import pyLDAvis.gensim\n",
    "\n",
    "vis = pyLDAvis.gensim.prepare(ldamodel, corpus=doc_term_matrix, dictionary=dictionary)\n",
    "pyLDAvis.display(vis)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Refrences:\n",
    "- <a href=\"https://pypi.org/project/lda/\"> LDA official site</a>.\n",
    "- <a href=\"https://rstudio-pubs-static.s3.amazonaws.com/79360_850b2a69980c4488b1db95987a24867a.html\"> LDA with Python</a>.\n",
    "- <a href=\"https://www.analyticsvidhya.com/blog/2016/08/beginners-guide-to-topic-modeling-in-python/\"> LDA on Python guide</a>.\n",
    "- <a href=\"https://github.com/bmabey/pyLDAvis/blob/master/tests/pyLDAvis/test_gensim_models.py\">test on gensim models</a>.\n",
    "- <a href=\"https://github.com/bmabey/pyLDAvis\"> pyLDAvis library</a>."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.7"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
